在我完成人臉關鍵點與人臉對齊的學習後,覺得眼睛有點累想要休息 -- 這時一個應用就出來了!
我們每天會接觸到"螢幕"的機會有很多:
而真正讓眼睛休息的時間,通常都是準備要就寢了。
適時的還是要讓眼睛做一些放鬆的運動,像是:眨眼放鬆
眼球轉動
凝視遠方
眼部肌肉按摩
...等等等
假設今天有一個App,就像內建的鬧鐘一樣,
會定期跳出提醒你:嘿!你好像用手機有點久了喔?來作一下眼部放鬆的運動吧!
只需要花個幾分鐘,
換來長時間用眼的舒緩,
我們就來作一個人臉互動的應用 -- 眼部放鬆App
(如果想要玩玩的可以到這裡看一下安裝與執行步驟,我們接下來將一步一步完成這個App)
- applications
- easy-eye-app
- utils
我們就依序開發各個方法吧!
utils
目錄,新增一個face_detector.py
(內容與之前Dlib MMOD的類似,只是改成class
方式):
# 匯入必要套件
import ntpath
import os
from bz2 import decompress
from urllib.request import urlretrieve
import cv2
import dlib
class FaceDetector:
def __init__(self):
# 下載模型檔案(.bz2)與解壓縮
model_name = "mmod_human_face_detector.dat"
model_path = os.sep.join([ntpath.dirname(ntpath.abspath(__file__)), model_name])
if not os.path.exists(model_path):
urlretrieve(f"https://github.com/davisking/dlib-models/raw/master/mmod_human_face_detector.dat.bz2",
model_name + ".bz2")
with open(model_name, "wb") as new_file, open(model_name + ".bz2", "rb") as file:
data = decompress(file.read())
new_file.write(data)
os.remove(model_name + ".bz2")
# 初始化模型
self._detector = dlib.cnn_face_detection_model_v1(model_path)
def detect(self, img):
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
results = self._detector(rgb, 1)
rects = [r.rect for r in results]
return rects
landmark_detector.py
(內容一樣與之前Dlib人臉關鍵點辨識的類似,只是改成class
方式);由於偵測人臉是使用Dlib MMOD方法,辨識人臉關鍵點就直接拿偵測人臉的Bounding Box來用就好:
import ntpath
import os
from bz2 import decompress
from urllib.request import urlretrieve
import cv2
import dlib
from imutils import face_utils
class LandmarkDetector:
def __init__(self, predictor_type):
if predictor_type == 5:
model_url = f"http://dlib.net/files/shape_predictor_5_face_landmarks.dat.bz2"
model_name = "shape_predictor_5_face_landmarks.dat"
elif predictor_type == 68:
model_url = f"https://github.com/davisking/dlib-models/raw/master/shape_predictor_68_face_landmarks_GTX.dat.bz2"
model_name = "shape_predictor_68_face_landmarks_GTX.dat"
else:
raise ValueError(f"un-support predictor type: {predictor_type}, must be 5 or 68!")
model_path = os.sep.join([ntpath.dirname(ntpath.abspath(__file__)), model_name])
if not os.path.exists(model_path):
urlretrieve(model_url, model_name + ".bz2")
with open(model_name, "wb") as new_file, open(model_name + ".bz2", "rb") as file:
data = decompress(file.read())
new_file.write(data)
os.remove(model_name + ".bz2")
# 初始化關鍵點偵測模型
self._predictor = dlib.shape_predictor(model_path)
def detect(self, img, rects):
shapes = []
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
for rect in rects:
shape = self._predictor(gray, rect)
shape = face_utils.shape_to_np(shape)
shapes.append(shape)
return shapes
hand_pose_estimator.py
:
import numpy as np
import cv2
# 3D 模型
model_points = np.array([
(0.0, 0.0, 0.0), # 鼻頭
(0.0, -330.0, -65.0), # 下巴
(-225.0, 170.0, -135.0), # 左眼中心
(225.0, 170.0, -135.0), # 右眼中心
(-150.0, -150.0, -125.0), # 嘴巴左邊中心
(150.0, -150.0, -125.0) # 嘴巴右邊中心
])
class HeadPoseEstimator:
def __init__(self, frame_width, frame_height):
self.frame_width = frame_width
self.frame_height = frame_height
@staticmethod
def _get_2d_points(rotation_vector, translation_vector, camera_matrix, dist_coeffs, val):
point_3d = []
rear_size = val[0]
rear_depth = val[1]
point_3d.append((-rear_size, -rear_size, rear_depth))
point_3d.append((-rear_size, rear_size, rear_depth))
point_3d.append((rear_size, rear_size, rear_depth))
point_3d.append((rear_size, -rear_size, rear_depth))
point_3d.append((-rear_size, -rear_size, rear_depth))
front_size = val[2]
front_depth = val[3]
point_3d.append((-front_size, -front_size, front_depth))
point_3d.append((-front_size, front_size, front_depth))
point_3d.append((front_size, front_size, front_depth))
point_3d.append((front_size, -front_size, front_depth))
point_3d.append((-front_size, -front_size, front_depth))
point_3d = np.array(point_3d, dtype=np.float).reshape(-1, 3)
# 將3D座標投影到2D平面上
(point_2d, _) = cv2.projectPoints(point_3d, rotation_vector, translation_vector, camera_matrix, dist_coeffs)
point_2d = np.int32(point_2d.reshape(-1, 2))
return point_2d
def _head_pose_points(self, rotation_vector, translation_vector, camera_matrix, dist_coeffs):
rear_size = 1
rear_depth = 0
front_size = self.frame_width
front_depth = front_size * 2
val = [rear_size, rear_depth, front_size, front_depth]
point_2d = self._get_2d_points(rotation_vector, translation_vector, camera_matrix, dist_coeffs, val)
p1 = point_2d[2]
p2 = (point_2d[5] + point_2d[8]) // 2
return p1, p2
def head_pose_estimate(self, shape):
face_3d_points = np.array([
shape[33], # 鼻頭
shape[8], # 下巴
shape[36], # 左眼中心
shape[45], # 右眼中心
shape[48], # 嘴巴左邊中心
shape[54] # 嘴巴右邊中心
], dtype="double")
# 粗估攝影機相關參數
focal_length = self.frame_width
center = (self.frame_width / 2, self.frame_height / 2)
camera_matrix = np.array([[
focal_length, 0, center[0]],
[0, focal_length, center[1]],
[0, 0, 1]], dtype="double")
# 假設攝影機都是已對焦
dist_coeffs = np.zeros((4, 1))
# 計算旋轉與轉換矩陣
(_, rotation_vector, translation_vector) = cv2.solvePnP(
model_points,
face_3d_points,
camera_matrix,
dist_coeffs,
flags=cv2.SOLVEPNP_ITERATIVE)
# 將一個"與臉部垂直"的3D座標投影到2D平面上
(nose_end_point2D, jacobian) = cv2.projectPoints(np.array([0.0, 0.0, 1000.0]), rotation_vector,
translation_vector, camera_matrix, dist_coeffs)
# 取得投影到2D平面的點 (後面用來計算臉部垂直方向角度)
vertical_p1 = (int(face_3d_points[0][0]), int(face_3d_points[0][1]))
vertical_p2 = (int(nose_end_point2D[0][0][0]), int(nose_end_point2D[0][0][1]))
# 取得水平方向角度用的座標
(horizontal_p1, horizontal_p2) = self._head_pose_points(rotation_vector, translation_vector, camera_matrix, dist_coeffs)
return vertical_p1, vertical_p2, horizontal_p1, horizontal_p2
到這邊前置作業已完成,明天我們將測試這些方法是否可以正常使用!